package ssi.peeno.managers;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;

import org.bouncycastle.util.encoders.Base64;



import ssi.peeno.PeenoWorker;
import ssi.peeno.messages.highlevel.*;
import ssi.peeno.model.Message;
import ssi.peeno.model.User;
import ssi.peeno.util.ByDistance;

public class MessageManager {
	private PeenoWorker myWorker;
	private UserManager usrManager;
	private SQLManager sqlManager;
	
	public MessageManager(String user, PeenoWorker worker, SQLManager sqlManager) {
		myWorker = worker;
		this.sqlManager = sqlManager; 
	}
	
	public MessageManager() {
	}

	public MessageManager(PeenoWorker worker, SQLManager sqlManager) {
		myWorker = worker;
		usrManager = worker.getUserManager();
		this.sqlManager = sqlManager;
	}

	public void onReceivedMsg(HighLevelMessage msg) {
		if (msg instanceof FriendAccept)
			friendAcceptReceived((FriendAccept)msg);
		else if (msg instanceof FriendRemove)
			friendRemoveReceived((FriendRemove)msg);
		else if (msg instanceof FriendRequest)
			friendRequestReceived((FriendRequest)msg);
		else if (msg instanceof FriendSearchRequest)
			friendSearchRecevied((FriendSearchRequest)msg);
		else if (msg instanceof FriendListRequest)
			friendListRequestReceived((FriendListRequest)msg);
		else if (msg instanceof MessageAdd)
			messageReceived((MessageAdd)msg);
		else if (msg instanceof MessageListRequest)
			messageListRequestReceived((MessageListRequest) msg);
		else if (msg instanceof MessageTextRequest)
			messageTextRequestReceived((MessageTextRequest) msg);
		else if (msg instanceof NotifyRequest){
			notifyRequestReceived((NotifyRequest)msg);
		} else if (msg instanceof LogoutMessage) {
			logoutReceived();
		}
		else {
			//procedura d'errore, messaggio sconosciuto (eccezione da mandare a worker?)
		}
	}
	
	private void logoutReceived() {
		usrManager.logoutUser(myWorker.getUser());
		myWorker.logoutUser();
	}

	private void notifyRequestReceived(NotifyRequest msg) {
		NotifyResponse response=new NotifyResponse();
		ArrayList<User> users = usrManager.getFriendsWhoAcceptMe(myWorker.getUser());
		HighLevelMessage[] messages = new HighLevelMessage[users.size()];
		FriendAcceptResponse m;
		for (int i=0; i<users.size();i++){
			m=new FriendAcceptResponse();
			m.setPub(new String(Base64.encode(users.get(i).getPubKey().getEncoded())));
			m.setUsername(users.get(i).getUsername());
			messages[i]=m;
		}
		if (myWorker.friendsDownloadForced){
			usrManager.restoreAcceptedFriends(myWorker.getUser());
		}
		response.setNotifyList(messages);
		myWorker.send(response);
	}

	private void messageTextRequestReceived(MessageTextRequest msg) {
		MessageTextResponse ret = new MessageTextResponse();
		ret.setText(loadMessageText(msg.getId()));
		myWorker.send(ret);
	}

	private String loadMessageText(int id) {
		String query = "SELECT msgText FROM Message WHERE msgID = " + id +" LIMIT 1;";
		ResultSet rs = sqlManager.returnQuery(query);
		String ret = "";
		try {
			while (rs.next())
				ret = rs.getString("msgText");
		} catch (SQLException e) {
			e.printStackTrace();
			return ret;
		}
		return ret;
	}

	private void messageListRequestReceived(MessageListRequest msg) {
		ConfigManager.log("MESSAGE LIST REQUEST");
		int what = msg.getWhat();
		ArrayList<Message> msgs = new ArrayList<Message>();
		if (what == 1) { // load public messages
			 msgs = loadPublicMessagesForUser(myWorker.getUser().getId(), 
				msg.getLat(), msg.getLon(), msg.getRadius());
		}
		else if (what == 2) {
			msgs = loadPrivateMessagesForUser(myWorker.getUser().getId(), 
					msg.getLat(), msg.getLon(), msg.getRadius());
		}
		MessageListResponse res = new MessageListResponse();
		res.setMessages(msgs.toArray(new Message[msgs.size()]));
		myWorker.send(res);
	}

	private void friendListRequestReceived(FriendListRequest msg) {
		FriendListResponse fr = new FriendListResponse();
		int what = msg.getWhat();
		ArrayList<User> friends = new ArrayList<User>();
		fr.setWhat(what);
		switch (what) {
			case 0: // Accepted/Response
				friends = usrManager.getFriends(myWorker.getUser());
				break;
			case 1: // RequestToMe
				friends = usrManager.getFriendsRequestToMe(myWorker.getUser());
				break;
			case 2: // RequestFromMe
				friends = usrManager.getFriendsRequestFromMe(myWorker.getUser());
				break;
		}
		User[] us= friends.toArray(new User[friends.size()]);
		fr.setUsers(us);
		myWorker.send(fr);
	}

	private void friendSearchRecevied(FriendSearchRequest msg) {
		ArrayList<User> found = usrManager.searchUser(msg.getCriteria(), myWorker.getUser());
		FriendSearchResponse resMsg = new FriendSearchResponse();
		resMsg.setUser(found.toArray(new User[found.size()]));
		myWorker.send(resMsg);
	}

	private void messageReceived(MessageAdd msg) {
		User receiver = null;
		if (!msg.getReceiver().equals("")) {
			receiver = usrManager.loadByUsername(msg.getReceiver());
			saveMessage(msg.getSubject(), msg.getText(), myWorker.getUser().getId(), 
				receiver.getId(), msg.getLat(), msg.getLon(), msg.getAlt(), msg.isCrypt());
		} else {
			saveMessage(msg.getSubject(), msg.getText(), myWorker.getUser().getId(), 
					0, msg.getLat(), msg.getLon(), msg.getAlt(), msg.isCrypt());
		}
	}
	
	private void friendRequestReceived(FriendRequest msg) {
		User friend = usrManager.loadByUsername(msg.getFriendUserName());
		usrManager.addFriend(myWorker.getUser(), friend, "REQUEST");
	}
	
	private void friendRemoveReceived(FriendRemove msg) {
		if (msg.isRefuse())
			usrManager.refuseFriendShip(myWorker.getUser(), msg.getFriendUserName());
		else 
			usrManager.removeFriend(myWorker.getUser(), msg.getFriendUserName());
	}

	private void friendAcceptReceived(FriendAccept msg) {
		User friend = usrManager.loadByUsername(msg.getFriendUserName());
		usrManager.acceptFriend(myWorker.getUser(), friend);
		FriendAcceptResponse res = new FriendAcceptResponse();
		res.setUsername(msg.getFriendUserName());
		res.setPub(myWorker.getKeyManager().loadRawUserKey(msg.getFriendUserName()));
		myWorker.send(res);
	}
	
	private void saveMessage(String subject, String text, int author, 
			int receiver, double lat, double lon, double alt, boolean crypt) {
		String query;
		if (crypt)
			query = "INSERT INTO Message (msgID, msgType, msgSubject, msgText, msgLat, msgLong, msgAlt) " +
				"VALUES (NULL, 'CRYPT', '" + subject + "', '" + text + "', '" + lat + "', '" + lon + "', '" + alt + "');";
		else
			query = "INSERT INTO Message (msgID, msgType, msgSubject, msgText, msgLat, msgLong, msgAlt) " +
				"VALUES (NULL, 'PLAIN', '" + subject + "', '" + text + "', '" + lat + "', '" + lon + "', '" + alt + "');";
		sqlManager.executeQuery(query);
		query = "SELECT * FROM Message WHERE msgSubject = '" + subject + "' AND msgText = '" + text + "' " +
				"AND msgLat = '" + lat + "' AND msgLong = '" + lon + "' AND msgAlt = '" + alt + "' LIMIT 1";
		ResultSet rs = sqlManager.returnQuery(query);
		int msgID = 0;
		try {
			rs.next();
			msgID = rs.getInt("msgID");
		} catch (SQLException e) {
			e.printStackTrace();
		}
		if (msgID != 0) {
			if (receiver == 0)
				query = "INSERT INTO MessageTo (mstID, msgID, mstFrom, mstTo) VALUES " +
					"(NULL, '" + msgID + "', '" + author+ "', NULL);";
			else 
				query = "INSERT INTO MessageTo (mstID, msgID, mstFrom, mstTo) VALUES " +
					"(NULL, '" + msgID + "', '" + author+ "', '" + receiver + "');";
			sqlManager.executeQuery(query);
		}
	}
	
	private ArrayList<Message> loadPrivateMessagesForUser(int id, double lat, double lon, float distance) {
		String query =
					"SELECT D.usrID, D.usrUsername, A.msgID, A.msgLat, A.msgLong, A.msgAlt, A.msgSubject, A.msgText, A.msgType, B.mstTo " +
					"FROM Message A " +
					"INNER JOIN MessageTo B ON A.msgID = B.msgID " +
					"INNER JOIN FriendsOf C ON B.mstFrom = C.frdUser2 " +
					"INNER JOIN User D ON C.frdUser2 = D.usrID " +
					"WHERE B.mstTo = " + id +" " +
					"AND C.frdUser1 = " + id +" " +
					"AND C.frdStatus = 'ACCEPTED'";
		return constructMessages(sqlManager.returnQuery(query), distance, lat, lon, id);
	}
	
	@SuppressWarnings("unchecked")
	private ArrayList<Message> loadPublicMessagesForUser(int id, double lat, double lon, float distance) {
		String query = 
				"SELECT D.usrID, D.usrUsername, A.msgID, A.msgLat, A.msgLong, A.msgAlt, A.msgSubject, A.msgText, A.msgType, B.mstTo " +
					"FROM Message A INNER JOIN MessageTo B ON A.msgID = B.msgID " +
					"INNER JOIN FriendsOf C ON B.mstFrom = C.frdUser2 " +
					"INNER JOIN User D ON C.frdUser2 = D.usrID " +
					"WHERE B.mstTo IS NULL AND C.frdUser1 = " + id +" " +
					"AND C.frdStatus = 'ACCEPTED' " +
				"UNION ALL " +
				"(" +
					"SELECT D.usrID, D.usrUsername, A.msgID, A.msgLat, A.msgLong, A.msgAlt, A.msgSubject, A.msgText, A.msgType, B.mstTo " +
					"FROM Message A " +
					"INNER JOIN MessageTo B ON A.msgID = B.msgID " +
					"INNER JOIN User D ON B.mstFrom = D.usrID " +
					"WHERE B.mstTo IS NULL " +
					"AND B.mstFrom = " + id + "" +
				")";
		return constructMessages(sqlManager.returnQuery(query), distance, lat, lon, id);
	}
	
	@SuppressWarnings("unchecked")
	private ArrayList<Message> constructMessages(ResultSet rs, double distance, double lat, double lon, int id) {
		ArrayList<Message> ret = new ArrayList<Message>();
		double dist;
		try {
			while (rs.next()) {
				if ((dist = distance(lat, lon, rs.getDouble("msgLat"), rs.getDouble("msgLong"))) <= distance) {
					Message m = new Message();
					m.setAuthor(rs.getString("usrUsername"));
					m.setDistance(dist);
					m.setId(rs.getInt("msgID"));
					m.setLat(rs.getDouble("msgLat"));
					m.setAlt(rs.getDouble("msgAlt"));
					m.setLon(rs.getDouble("msgLong"));
					m.setSubject(rs.getString("msgSubject"));
					m.setType(rs.getString("msgType"));
					if (rs.getInt("mstTo") != 0)
						m.setPrivate(true);
					else
						m.setPrivate(false);
					if (rs.getInt("usrID") == id)
						m.setMyMessage(true);
					else 
						m.setMyMessage(false);
					ret.add(m);
				}
				else {
					ConfigManager.log("Msg(" + rs.getInt("msgID") + ") scartato distanza: " + dist);
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
			Collections.sort(ret, new ByDistance());
			return ret;
		}
		Collections.sort(ret, new ByDistance());
		return ret;
	}

	private double distance(double lat1, double lon1, double lat2, double lon2) {
		double radius = 6371; //Km
		double dlat=Math.toRadians(lat2-lat1);
		double dlon=Math.toRadians(lon2-lon1);
		double preDistance=Math.sin(dlat/2) * Math.sin(dlat/2) + Math.sin(dlon/2) * Math.sin(dlon/2) * Math.cos(Math.toRadians(lat1))*Math.cos(Math.toRadians(lat2));
		double semiDistance = 2*Math.atan2(Math.sqrt(preDistance), Math.sqrt(1-preDistance));
		double distance=semiDistance*radius;
		return (distance<0.001)?0.0:distance;
	}	
}
